Skip to content

Allow Whisper decoding to use the full decoder context - #2075

Open
Emre-Akgul wants to merge 4 commits into
OpenNMT:masterfrom
Emre-Akgul:agent/whisper-no-timestamps-full-context
Open

Allow Whisper decoding to use the full decoder context#2075
Emre-Akgul wants to merge 4 commits into
OpenNMT:masterfrom
Emre-Akgul:agent/whisper-no-timestamps-full-context

Conversation

@Emre-Akgul

@Emre-Akgul Emre-Akgul commented Jul 11, 2026

Copy link
Copy Markdown

Summary

Whisper decoding unconditionally limited generation to half of max_length, regardless of whether timestamps were enabled. This PR removes that half-context cap for both no-timestamp and timestamped decoding, letting each use the full decoder context that remains after the prompt.

This improves transcription completeness for languages that rely heavily on Whisper's byte-level fallback tokenizer, where relatively short audio can consume a large number of decoder tokens.

Fixes #2074, #1856.

Root Cause

Whisper generation set:

decoding_options.max_length = std::min(
    total_max_length / 2,
    total_max_length - start_step
);

total_max_length - start_step is the real safety bound — it's what actually keeps start_step + max_length from exceeding the model's fixed 448-token context (n_text_ctx), and it already existed independently of the /2 term. The /2 term is a second, stricter ceiling stacked on top of it.

Change

Both decoding paths now use the entire remaining decoder context:

decoding_options.max_length = total_max_length - start_step;

With the standard 3-4 token prompt, decoding can now use up to ~445 positions instead of the previous 224, for both no-timestamp and timestamped generation.
Does this risk overflowing the 448-token context, or otherwise change output on cases that already worked? No. total_max_length - start_step is unchanged from before — it already guastart_step + max_length == total_max_length exactly, in both the old and new codeonly make the allowed length smaller-or-equal to what that bound already permitted,never larger, so there's no new overflow path. And it has no effect at all on audio that naturally finishes (emits <|endoftext|>) before hitting 224 tokens, which is the common case for typical-length audio.

Reproduction

Both reproductions use a direct, single-window call (`ctranslate2.models.Whisper.ge the standard 30s/3000-frame window).

Model: Systran/faster-whisper-large-v3, same for both modes. Armenian and Georgian rely heavily on Whisper's byte-level fallbacktokenizer, so even relatively short audio can exhaust the previous 224-token limit — the Armenian reference needs 285 text tokens, the Georgian reference needs 516 (more than the model's complete 448-token context can ever hold, even after this fix).

No-timestamp decoding (without_timestamps=True)

Armenian — before fix (224 tokens, cuts off mid byte-sequence):

Մումբայի հարցակվողները կաղակ ժամանեցին նավակով, իրենց հետ բերելով նրնակներ, ինքնածիկ սենքեր և հարվածեցին բազմաթիվ թիրախների, որոնց թվում էին մարդաշատ չյատրատ պատի շիվաջի տեր մինուս երկատգծի կայարան�

Armenian — after fix (261 tokens, completes normally):

Մումբայի հարցակվողները կաղակ ժամանեցին նավակով, իրենց հետ բերելով նրնակներ, ինքնածիկ սենքեր և հարվածեցին բազմաթիվ թիրախների, որոնց թվում էին մարդաշատ չյատրատ պատի շիվաջի տել մինուս երկատգծի կայարանը
և հայտնի թաճ մահալ հյուրանոցը։

Georgian — before fix (224 tokens, cuts off):

იგիც ինասდარ պատი մրობաშია საბրալდებუ დասყვნისა და სასამართლო პროცესის մոլოդինში, თ

Georgian — after fix (445 tokens, uses the full decoder budget, still cuts off — the reference needs 516 tokens, more than the model's 448-token context can ever hold):

იგიცე, ენასდარ პატიმრობაშია, საბრალდებო დასყვნისა 16-ს პროცესის მოლოდინში თუმცა მისექ მოცმეების შეიძლება არა კეთელს ინდისიერ, მთქითე ბულე ბად აყიარონ. გი�

Timestamped decoding (default, no <|notimestamps|>)

Armenian — before fix (224 tokens, cuts off mid word):

<|0.00|> Երկու հազարութվականի նոյմբերի 26-ին Մումբայի հարցակվողները կաղակ ժամանեցինրնակներ, ինքնածիկ սենքեր և հարվածեցին բազմաթիվ թիրախների,<|12.86|><|12.86|> որոնց թվում էին մարդաշատ չյատրատպա�

Armenian — after fix (304 tokens, completes fully — decoding stops exactly at the|>`):

<|0.00|> Երկու հազարութվականի նոյմբերի 26-ին Մումբայի հարցակվողները կաղակ ժամանեցին նավակով, իրենց հետ բերելով նրնակներ, ինքնածիկ սենքեր և հարվածեցին բազմաթիվ թիրախների,<|12.86|><|12.86|> որոնց թվում
էին մարդաշատ չյատրատպատի շիվաջի տեռ մինուս երկատգծի կայարանը և հայտնի թաճ մահալ հյո

Georgian — before fix (224 tokens, cuts off):

<|0.00|> იგიც ინასდარ პატიმრობაშია საბრალდებუ დასყვნისა და სასამართლო პროცესის მოლომი

Georgian — after fix (446 tokens, reaches the full decoder budget — still cuts off for the same reason as the no-timestamp case above):

<|0.00|> იგიც ინასდარ პატიმრობაშია საბრალდებუ დასყვნისა და სასამართლო პროცესის მოლომისი პირონების გასაჯარვების შემდეგ მოცმეების ნების მიარი ჩვენება შეიძლება არა კეთილსინდი�

(The timestamped Georgian result reaches 446 rather than 445 because its prompt is -timestamp prompt — it omits <|notimestamps|> — leaving one extra position ofcontext.)

Interpretation

The Armenian sample demonstrates the intended fix clearly in both modes: the transcription needs more than 224 tokens but fits within the remaining decoder context, so it now completes normally
instead of stopping mid-byte-sequence.

The Georgian sample exceeds the model's complete 448-token decoder context even remains truncated, but the decoder now returns substantially more of the transcription than the artificial 224-token cap previously allowed.

Validation

  • The C++ library and Python bindings build successfully.
  • The existing Whisper integration test passes all three parameter sets.
  • Regression test (test_transformers_whisper_full_context, whisper-tiny) update and timestamped decoding now reach the full remaining context (445 / 446 positions)instead of the old 224 cap for timestamped decoding.
  • Patched large-v3 decoding validated end-to-end on the Armenian and Georgian samtamp and timestamped decoding.

@Emre-Akgul

Copy link
Copy Markdown
Author

Hi @jordimas, when you have a chance, would you mind taking a look at this PR? I’d really appreciate any feedback. Thanks!

@jordimas

Copy link
Copy Markdown
Collaborator

@Emre-Akgul please add a regression test covering this behavior: no-timestamp decoding can exceed the previous 224-token cap, while timestamped decoding keeps the existing limit.

@jordimas

Copy link
Copy Markdown
Collaborator

@MahmoudAshraf97 @Purfview see in case you have any comment

@MahmoudAshraf97

Copy link
Copy Markdown
Contributor

The previous 224 token cap was in the reference open ai implementation, the model itself tolerates up to 448, that includes both input and output tokens, so as long as the total token count does not exceed that it's fine, note that whisper passes 4 tokens + the previous transcription, which are included in the 448
The timestamps are not the reason why that limit exists

Covers the behavior requested in review: no-timestamp decoding can
exceed the previous 224-token cap (up to 445), while timestamped
decoding keeps the existing 224-token limit.
@Emre-Akgul Emre-Akgul closed this Aug 29, 2026
@Emre-Akgul
Emre-Akgul deleted the agent/whisper-no-timestamps-full-context branch August 29, 2026 21:05
@Emre-Akgul Emre-Akgul reopened this Aug 29, 2026
@Emre-Akgul

Copy link
Copy Markdown
Author

@MahmoudAshraf97 Thanks for the correction, you're right that the 224 cap isn't tied to timestamps. Updated the PR description's "Root Cause" section to reflect that it's just half of n_text_ctx inherited from OpenAI's reference implementation, not a timestamp requirement. The fix stays within the 448-token total budget you described, just uses what's left of it instead of an unconditional half.

@Emre-Akgul

Copy link
Copy Markdown
Author

@jordimas Added a regression test (test_transformers_whisper_no_timestamps_full_context in python/tests/test_transformers.py) covering exactly that: no-timestamp decoding reaches 445 positions, timestamped decoding stays capped at 224. Verified it fails against the pre-fix code and passes with the fix. Kept timestamped decoding untouched since it's out of scope for the PR.

@MahmoudAshraf97

Copy link
Copy Markdown
Contributor

Kept timestamped decoding untouched since it's out of scope for the PR.

This is inconsistency, since the limit was not related to timestamps, why is that separation still there?

@Purfview

Purfview commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

As I remember, length limits are handled at the faster-whisper level, why not just do:

decoding_options.max_length = total_max_length - start_step;

And use condition_on_previous_text=False in faster-whisper if you need more tokens with those languages.

But I wonder why there was 224 cap in the first place?
Wouldn't there be some other possible side effects with this change?

@MahmoudAshraf97

Copy link
Copy Markdown
Contributor

But I wonder why there was 224 cap in the first place? Wouldn't there be some other possible side effects with this change?

OpenAI implementation has it to accommodate the previous transcription prompts because it always truncated the prompt to 224 tokens which is half

@Emre-Akgul Emre-Akgul changed the title Allow no-timestamp Whisper decoding to use full context Allow Whisper decoding to use the full decoder context Aug 30, 2026
The half-context cap removed for no-timestamp decoding was never
actually tied to timestamps - it originated as half of n_text_ctx in
OpenAI's reference implementation and applied regardless of the
<|notimestamps|> prompt token. Timestamped decoding now also uses the
full remaining decoder context (total_max_length - start_step).

Verified on large-v3 with the Armenian/Georgian samples from the
previous commit using timestamped prompts: Armenian now completes
fully (304 tokens vs. the previous 224-token cutoff), and Georgian
reaches the full ~445-token decoder budget instead of stopping at 224.
@Emre-Akgul
Emre-Akgul force-pushed the agent/whisper-no-timestamps-full-context branch from b241df9 to 6d8b164 Compare August 30, 2026 10:58
@Emre-Akgul

Copy link
Copy Markdown
Author

Kept timestamped decoding untouched since it's out of scope for the PR.

This is inconsistency, since the limit was not related to timestamps, why is that separation still there?

Applied the same fix to timestamped decoding in 6d8b164: both paths now use total_max_length - start_step instead of the /2 cap.

Verified on large-v3 with the same Armenian/Georgian samples, now with timestamps enabled — Armenian goes from cutting off at 224 tokens to completing fully at 304 tokens (ending exactly at the audio's real timestamp), and Georgian reaches the full ~445-token budget instead of stopping at 224. Details and before/after examples for both modes are in the updated PR description.

@Emre-Akgul

Copy link
Copy Markdown
Author

As I remember, length limits are handled at the faster-whisper level, why not just do:

decoding_options.max_length = total_max_length - start_step;

And use condition_on_previous_text=False in faster-whisper if you need more tokens with those languages.

But I wonder why there was 224 cap in the first place? Wouldn't there be some other possible side effects with this change?

Even with condition_on_previous_text=False, the CTranslate2 code still capped generation at 224 tokens. The cap lived entirely inside ctranslate2's C++ code, not in faster-whisper's Python layer, so nothing faster-whisper does with its prompt/conditioning logic could route around it.

@Purfview

Purfview commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Even with condition_on_previous_text=False, the CTranslate2 code still capped generation at 224 tokens. The cap lived entirely inside ctranslate2's C++ code, not in faster-whisper's Python layer, so nothing faster-whisper does with its prompt/conditioning logic could route around it.

Sure, probably it wasn't clear but that's what I meant.
Shouldn't you revert the edits on the other lines? In your last commit, you no longer use the without_timestamps condition for decoding_options.max_length.

Could you check whether the PR have any speed impact on difficult audio/languages with lots of non-speech? I'm thinking of something like 70 years old Icelandic audio for example. Of course, this should be tested without VAD.

The without_timestamps local was introduced to share a condition with
the max_length ternary, which the previous commit already removed.
Its only remaining use (gating the ApplyTimestampRules logits
processor) is unrelated to this PR's max_length fix, so revert that
check back to its original inline form per review feedback.
@Emre-Akgul

Copy link
Copy Markdown
Author

Even with condition_on_previous_text=False, the CTranslate2 code still capped generation at 224 tokens. The cap lived entirely inside ctranslate2's C++ code, not in faster-whisper's Python layer, so nothing faster-whisper does with its prompt/conditioning logic could route around it.

Sure, probably it wasn't clear but that's what I meant. Shouldn't you revert the edits on the other lines? In your last commit, you no longer use the without_timestamps condition for decoding_options.max_length.

Could you check whether the PR have any speed impact on difficult audio/languages with lots of non-speech? I'm thinking of something like 70 years old Icelandic audio. Of course, this should be tested without VAD.

You're right. Reverted the without_timestamps refactor.

On the speed question: for audio where the decoder naturally emits <|endoftext|> before 224 tokens (the common case), this change has zero effect — the old and new caps are both irrelevant since decoding stops earlier either way. The case where it matters: long, non-speech audio without VAD where the decoder hallucinates/repeats instead of hitting EOT naturally. There, the cap now allows ~2x more tokens before stopping, which means a slowdown on that specific pathological case. I haven't benchmarked it because I got no data at hand.

@Purfview

Purfview commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

I haven't benchmarked it because I got no data at hand.

Here is an audio for a test: https://www.transfernow.net/dl/20260830loYkBzko
Use condition_on_previous_text=False, beam_size=5, vad_filter=False, and do ~3-5 runs to get average timings.

@Emre-Akgul

Copy link
Copy Markdown
Author

I haven't benchmarked it because I got no data at hand.

Here is an audio for a test: https://www.transfernow.net/dl/20260830loYkBzko Use condition_on_previous_text=False, beam_size=5, vad_filter=False, and do ~3-5 runs to get average timings.

Ran the benchmark.
Setup:

  • Audio: the Icelandic file you linked (~81 min, 4874.6s), no VAD
  • faster-whisper 1.2.1 (same version for both sides), beam_size=5, condition_on_previous_text=False, vad_filter=False
  • Model: large-v3, GPU (RTX 3060 Mobile), float16
  • Baseline: official ctranslate2 4.8.1 from PyPI (pre-fix)
  • PR build: this branch built locally against the same source tree
  • 3 runs each, full pass over the whole file per run
Run Baseline (pre-fix) PR build (fix)
1 376.18s 583.27s
2 308.14s 487.09s
3 420.09s 580.62s
avg 368.14s 550.33s
min / max 308.14s / 420.09s 487.09s / 583.27s

So the tradeoff comes down to this: for underrepresented languages that lean hard on the byte-level fallback tokenizer, the old 224-token cap could cut a correct transcript off mid-word, so the fix gets those languages a complete result. But when it hallucinates instead, you wait ~1.5x longer and get ~2x more junk output tokens for it.

@Purfview

Purfview commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

avg 368.14s 550.33s

Huge speed diff, but it was expected in such edge case. Thanks for the test.

Could you run one more test using non-difficult audio (a book)?
Use condition_on_previous_text=False, beam_size=5, vad_filter=False, temperature=0. 1 run would be enough.
I’m also interested in whether the transcription outputs(timestamped) are the same. Please share outputs (in files) if they differ.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow no-timestamp Whisper decoding beyond 224 tokens

4 participants